/*
 * Copyright 2011 Angel Sanadinov
 *
 * This file is part of VBox WMI.
 *
 * VBox WMI is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * VBox WMI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with VBox WMI.  If not, see <http://www.gnu.org/licenses/>.
 */

package Managers;

import javax.servlet.http.HttpSession;
import Beans.UserBean;
import Beans.PermissionsBean;
import Managers.Utilities.ResponseMessagesContainer;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import Managers.Utilities.User;
import Utilities.ConfigurationReader;
import Utilities.Constants;
import Utilities.ParameterNames;
import Utilities.Interfaces.Controllable;
import Utilities.Interfaces.SelfValidating;

/**
 * Manager for user sessions. <br><br>
 * A user session is considered valid only if it has been registered with this manager.
 *
 * @see SessionsManager#loginUser(int, javax.servlet.http.HttpSession) User Login
 *
 * @author Angel Sanadinov
 */
public class SessionsManager implements SelfValidating, Controllable
{
    private Map<Integer, User> users; //map of all users that have logged in
    private DatabaseManager database; //reference to the database manager
    private LogsManager logsManager;  //reference to the logs manager
    private VBoxConnectionsManager connectionsManager; //reference to the connections manager
    private int maxResponseMessages; //the max number of request/response object to be held in the container

    /**
     * Constructs the manager using the supplied arguments. <br><br>
     *
     * A synchronized <code>HashMap</code> is used to store data about each user
     * that has logged in and the initial capacity and load factor can be set to aid
     * the scalability of the application. Refer to the <code>HashMap</code>
     * documentation for more information.
     *
     * @param database reference to the database manager
     * @param logger reference to the logs manager
     * @param conManager reference to the VirtualBox connections manager
     * @param configuration configuration parameters for the manager
     */
    public SessionsManager(DatabaseManager database, LogsManager logger,
                           VBoxConnectionsManager conManager, ConfigurationReader configuration)
    {
        this.database = database;
        this.logsManager = logger;
        this.connectionsManager = conManager;

        int mapInitialCapacity = 0;
        float mapLoadFactor = 0.75f; //default HashMap setting
        this.maxResponseMessages = 1;

        try
        {
            mapInitialCapacity = Integer.parseInt(
                    configuration.getParameter(ConfigurationReader.PARAM_SESSIONS_USERS_MAP_INITIAL_CAPACITY));

            mapLoadFactor = Float.parseFloat(
                    configuration.getParameter(ConfigurationReader.PARAM_SESSIONS_USERS_MAP_LOAD_FACTOR));

            maxResponseMessages = Integer.parseInt(
                    configuration.getParameter(ConfigurationReader.PARAM_MAX_RESPONSE_MESSAGES));
        }
        catch(NumberFormatException e)
        {
            logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_WARNING,
                                       "Sessions Manager: Failed to parse initialization parameter(s). "
                                       + "Values: parameter(C:current/D:default); "
                                       + "initialCapacity(C:"+mapInitialCapacity+"/D:0); "
                                       + "loadFactor(C:"+mapLoadFactor+"/D:0.75); "
                                       + "maxResponseMessages(C:"+maxResponseMessages+"/D:1);");
        }
        
        //creates a synchronized HashMap with the supplied parameters
        this.users = Collections.synchronizedMap(new HashMap<Integer, User>(mapInitialCapacity, mapLoadFactor));
        this.logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_INFORMATION, "Sessions Manager started...");
    }

    /**
     * Verifies that the user bound to the supplied session object is registered
     * with the sessions manager.
     *
     * @param userSession the user session
     *
     * @return <code>true</code> if the user in the supplied session matches a
     *         user registered with the manager.
     */
    public boolean verifyUserSession(HttpSession userSession)
    {
        boolean result = false;

        //checks if a user session is provided
        if(userSession != null)
        {
            //retrieves the user object from the session
            User userFromSession = (User)userSession.getAttribute(ParameterNames.SESSION_USER_OBJECT);

            //checks if the object is valid
            if(userFromSession != null && userFromSession.isValid())
            {
                //retrieves the user object from the map
                User userFromMap = users.get(userFromSession.getUserData().getUserId());

                //checks if the user object from the map is available and if the user ids match
                if(userFromMap != null && userFromMap.getUserData().getUserId() == userFromSession.getUserData().getUserId())
                    result = true;
                else
                    ;
            }
            else
                ;

            return result;
        }
        else
            return false;
    }

    /**
     * Logs in a user into the system. <br><br>
     * <b>Note:</b> <i>Authentication should be performed before this method is
     * called!</i>
     *
     * @param userId the id of the user to be logged in
     * @param userSession the server session associated with that user
     * @param userRemoteAddress the IP address of the user to be logged in
     *
     * @return <code>true</code> if the user was successfully logged in or
     *         <code>false</code> otherwise
     *
     * @see SessionsManager Sessions Manager
     */
    public boolean loginUser(int userId, HttpSession userSession, String userRemoteAddress)
    {
        //builds the user object based on the data retrieved from the database
        User newUser = new User(database.getUser(userId), userSession, this, database.getAllPermissionsForUser(userId));
        ResponseMessagesContainer messagesContainer = new ResponseMessagesContainer(maxResponseMessages);

        //checks if the user data is valid and if the user's account is disabled
        if(newUser.isValid() && newUser.getUserData().getAccountLevel() > Constants.DB_USER_ACCOUNT_LEVEL_DISABLED
           && newUser.getUserData().getUserId() == userId)
        {
            //puts the user in the map
            users.put(userId, newUser);
            //sets the user object as a session attribute
            userSession.setAttribute(ParameterNames.SESSION_USER_OBJECT, newUser);
            userSession.setAttribute(ParameterNames.SESSION_REQUEST_MESSAGES_OBJECT, messagesContainer);
            //logs the event
            logsManager.log(System.currentTimeMillis(),
                            Constants.LOG_SEVERITY_USER_LOGIN,
                            "User logged in from " + userRemoteAddress,
                            userId);
            logsManager.logSystemEvent(System.currentTimeMillis(),
                                       Constants.LOG_SEVERITY_DEBUG,
                                       "User logged in (" + userId + "," + userRemoteAddress + ")");
            //updates the database to reflect the changes in the last login date of the user
            database.updateUserLastLogin(userId, userRemoteAddress);
            return true;
        }
        else //user is not logged in
            return false;
    }

    /**
     * Logs out a user by invalidating the associated session.
     *
     * @param userId the id of the user that is logging out
     * @param userSession the session of the user
     *
     * @return <code>true</code> if the user was successfully logged out
     */
    public boolean logoutUser(int userId, HttpSession userSession)
    {
        //retrieves the user object from the map
        User userLoggingOut = users.get(userId);

        //checks if the user object is available and if the session ids match
        if(userLoggingOut != null && userLoggingOut.compareSessionId(userSession.getId()))
        {
            //invalidates the user session
            userSession.invalidate();
            return true;
        }
        else
            return false;
    }

    /**
     * Removes the user object from the map. <br><br>
     *
     * <b>Note:</b> <i><code>logoutUser()</code> should be used for regular user logouts!</i>
     *
     * @param user the user object to be removed from the map
     * @see SessionsManager#logoutUser(int, javax.servlet.http.HttpSession) User Logout
     */
    public void unregisterUser(User user)
    {
        //checks if the supplied object is valid and if it is in the users map
        if(user.isValid() && users != null && users.containsValue(user))
        {
            //object is removed
            users.remove(user.getUserData().getUserId());
            //unregisters the user from any virtualbox connections he/she used
            connectionsManager.unregisterConnectionUser(user.getUserData().getUserId());
        }
        else
            ;
    }

    /**
     * Updates the data that is stored in the specified user's session. <br><br>
     *
     * The data for the specified user is retrieved from the database and the user
     * session is updated with it.
     * <br><br>
     * <b>Note:</b> <i>This method should only be called when the user data was
     *                 modified and an update from the database is necessary.</i>
     *
     * @param userId the id of the user that needs to be updated
     */
    public void updateUserData(int userId)
    {
        User user = users.get(userId); //retrieves the data user from the map

        if(user != null ) //checks if the user was found
        {
            user.updateUserData(database.getUser(userId)); //updates the user data

            //checks if the user was disabled during the update
            if(user.getUserData().getAccountLevel() <= Constants.DB_USER_ACCOUNT_LEVEL_DISABLED)
                logoutUser(userId, user.getUserSession()); //the user is logged out
            else
                ;
            logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_DEBUG,
                                       "Data was updated for user: " + userId);
        }
        else
            ;
    }

    /**
     * Updates the permissions of the specified user for the specified server. <br><br>
     *
     * The permissions data for the specified user and server is retrieved from
     * the database and the user session is updated with it.
     * <br><br>
     * <b>Note:</b> <i>This method should only be called when the permissions were
     *                 modified and an update from the database is necessary.</i>
     * @param userId the id of the user
     * @param serverId the server associated with the permissions
     */
    public void updateUserPermissions(int userId, int serverId)
    {
        User user = users.get(userId); //retrieves the data user from the map

        if(user != null) //checks if the user was found
        {
            //retrieves the permissions data
            PermissionsBean permissions = database.getPermissions(userId, serverId);

            //checks if the permissions are valid
            if(permissions != null && permissions.isValid())
                user.updatePermissions(permissions); //updates the permissions
            else //no permissions were found (invalid request or they were removed)
                user.removePermissions(serverId); //removes the permissions from the session

            logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_DEBUG,
                                       "Permissions were updated for user: " + userId);
        }
        else
            ;
    }

    /**
     * Retrieves the permissions for the specified user on the specified server.
     *
     * @param userId the id of the user
     * @param serverId the id of the server
     *
     * @return the requested permissions or <code>null</code> if they could not
     *         be retrieved
     */
    public PermissionsBean getPermissionsForUserOnServer(int userId, int serverId)
    {
        PermissionsBean result = null;
        User user = users.get(userId); //retrieves the user from the map

        //checks if the user is available
        if(user != null)
            result = user.getPermissions(serverId); //retrieves the permissions
        else
            ;
        
        return result;
    }

    /**
     * Retrieves user data based on the id of the user.
     *
     * @param userId the id of the user
     *
     * @return the requested user data or <code>null</code> if it could not be retrieved
     */
    public UserBean getUserData(int userId)
    {
        UserBean result = null;
        User user = users.get(userId); //retrieves the user from the map

        //checks if the user is available
        if(user != null)
            result = user.getUserData();//retrieves the user data object
        else
            ;

        return result;
    }

    /**
     * Checks the validity of the manager. <br><br>
     * The manager is valid only if the reference to the required managers are available
     * and the users map is initialized.
     *
     * @return <code>true</code> if the manager is valid
     */
    @Override
    public boolean isValid()
    {
        if(users != null && database != null && connectionsManager != null && logsManager != null)
            return true;
        else
            return false;
    }

    /**
     * Stops the manager and removes any associated resource references.
     */
    @Override
    public void stop()
    {
        logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_INFORMATION, "Sessions Manager stopping...");

        users.clear();
        users = null;
        database = null;
        logsManager = null;
        connectionsManager = null;
    }
}
